package ascloud.auth.config;

import static org.springframework.security.config.Customizer.withDefaults;

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;

import lombok.extern.slf4j.Slf4j;

import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer.authorizationServer;

@Slf4j
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JwkProperties.class)
public class AuthorizationServerConfig {

	private static final String LOGIN_FORM_URL = "/login";
	private static final String CONSENT_PAGE_URL = "/oauth2/consent";

	private static final List<String> ALLOWED_HEADERS = List.of("Access-Control-Allow-Origin", "x-requested-with");
	private static final List<String> ALLOWED_METHODS = List.of("POST", "GET");
	private static final List<String> ALLOWED_ALL = List.of("http://filbert-oauth2-resource-foo:8091",
			"http://filbert-oauth2-resource-bar:8092");

	@Value("${jwk.public-key}")
	public RSAPublicKey publicKey;

	@Value("${jwk.private-key}")
	public RSAPrivateKey privateKey;

	@Bean
	CorsConfigurationSource corsConfigurationSource() {
		CorsConfiguration configuration = new CorsConfiguration();
		configuration.setAllowedOrigins(ALLOWED_ALL);
		configuration.setAllowedMethods(ALLOWED_METHODS);
		configuration.setAllowedHeaders(ALLOWED_HEADERS);
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration);
		return source;
	}

	@Bean
	// DefaultOidcUserInfoMapper
	Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper(UserDetailsService service) {
		return context -> {
			OAuth2AccessToken accessToken = context.getAccessToken();
			OAuth2Authorization authorization = context.getAuthorization();
			Set<String> scopes = accessToken.getScopes();
			String name = authorization.getPrincipalName();
			UserDetails user = service.loadUserByUsername(name);

			Map<String, Object> claims = new HashMap<>();
			claims.put(StandardClaimNames.SUB, name);

			if (scopes.contains(OidcScopes.PROFILE)) {
				claims.put(StandardClaimNames.NAME, user.getUsername());
			}

			return new OidcUserInfo(claims);
		};
	}

	@Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
			Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper) throws Exception {

//		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
		OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = authorizationServer();
		// @formatter:off
		http
			.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
			.with(authorizationServerConfigurer, (authorizationServer) -> authorizationServer
				.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
					// DefaultConsentPage.displayConsent
					.consentPage(CONSENT_PAGE_URL))
				.oidc(oidc -> oidc
					.providerConfigurationEndpoint(providerConfigurationEndpoint -> providerConfigurationEndpoint
						.providerConfigurationCustomizer(providerConfigurationCustomizer -> providerConfigurationCustomizer
							.scope(OidcScopes.PROFILE)
							.scope(OidcScopes.EMAIL)
							.scope(OidcScopes.ADDRESS)
							.scope(OidcScopes.PHONE)
							.scope("springdoc.read")
							.scope("springdoc.write")))
					.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
						.userInfoMapper(userInfoMapper))))
			.authorizeHttpRequests(authorize -> authorize
				.anyRequest().authenticated());
//			.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
//			.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
//				// DefaultConsentPage.displayConsent
//				.consentPage(CONSENT_PAGE_URL))
//			.oidc(oidc -> oidc
//				.providerConfigurationEndpoint(providerConfigurationEndpoint -> providerConfigurationEndpoint
//					.providerConfigurationCustomizer(providerConfigurationCustomizer -> providerConfigurationCustomizer
//						.scope(OidcScopes.PROFILE)
//						.scope(OidcScopes.EMAIL)
//						.scope(OidcScopes.ADDRESS)
//						.scope(OidcScopes.PHONE)
//						.scope("springdoc.read")
//						.scope("springdoc.write")))
//				.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
//					.userInfoMapper(userInfoMapper)));
		
		http
			.cors(withDefaults())
			.exceptionHandling(exceptions -> exceptions
				.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint(LOGIN_FORM_URL)))
//				.defaultAuthenticationEntryPointFor(
//					new LoginUrlAuthenticationEntryPoint(LOGIN_FORM_URL),
//					new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
//				.oauth2ResourceServer(resourceServer -> resourceServer
//						.jwt(Customizer.withDefaults()))
		;
		// @formatter:on
		return http.build();
	}

//	@Bean
//	RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
//		
//		// @formatter:off
//		RegisteredClient fooClient = RegisteredClient.withId(UUID.randomUUID().toString())
//				.clientName("client-foo")
//				.clientId("foo")
//				.clientSecret("{noop}fooSecret")
//				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
//				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
//				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
//				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
//				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//				.redirectUri("http://filbert-oauth2-resource-foo:8091/swagger-ui/oauth2-redirect.html")
//				.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
//				.scope(OidcScopes.OPENID)
//				.scope(OidcScopes.PROFILE)
//				.scope(OidcScopes.EMAIL)
//				.scope(OidcScopes.ADDRESS)
//				.scope(OidcScopes.PHONE)
//				.scope("springdoc.read")
//				.scope("springdoc.write")
//				.clientSettings(ClientSettings.builder()
//						.requireAuthorizationConsent(true)
//						.build())
//				.tokenSettings(TokenSettings.builder()
//						.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
//						.authorizationCodeTimeToLive(Duration.ofMinutes(2))
//						.accessTokenTimeToLive(Duration.ofMinutes(5))
//						.refreshTokenTimeToLive(Duration.ofMinutes(10))
//						.reuseRefreshTokens(true)
//						.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
//						.build())
//				.build();
//
//		RegisteredClient barClient = RegisteredClient.withId(UUID.randomUUID().toString())
//				.clientName("client-bar")
//				.clientId("bar")
//				.clientSecret("{noop}barSecret")
//				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
//				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
//				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
//				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
//				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
//				.redirectUri("http://filbert-oauth2-resource-bar:8092/swagger-ui/oauth2-redirect.html")
//				.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
//				.scope(OidcScopes.OPENID)
//				.scope(OidcScopes.PROFILE)
//				.scope(OidcScopes.EMAIL)
//				.scope(OidcScopes.ADDRESS)
//				.scope(OidcScopes.PHONE)
//				.scope("springdoc.read")
//				.scope("springdoc.write")
//				.clientSettings(ClientSettings.builder()
//						.requireAuthorizationConsent(true)
//						.build())
//				.tokenSettings(TokenSettings.builder()
//						.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
//						.authorizationCodeTimeToLive(Duration.ofMinutes(20))
//						.accessTokenTimeToLive(Duration.ofMinutes(50))
//						.refreshTokenTimeToLive(Duration.ofMinutes(100))
//						.reuseRefreshTokens(true)
//						.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
//						.build())
//				.build();
//		// @formatter:on
//
//		// Save registered client in db as if in-memory
//		JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
//
//		registeredClientRepository.save(fooClient);
//		registeredClientRepository.save(barClient);
//
//		return registeredClientRepository;
//	}

	@Bean
	OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
			RegisteredClientRepository registeredClientRepository) {
		return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
	}

	@Bean
	OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
			RegisteredClientRepository registeredClientRepository) {
		return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
	}

	@Bean
	OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
		return context -> {
			OAuth2TokenType tokenType = context.getTokenType();
			AuthorizationGrantType grantType = context.getAuthorizationGrantType();

			if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(grantType)
					|| AuthorizationGrantType.REFRESH_TOKEN.equals(grantType)) {
				if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)
						|| OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {

					Authentication principal = context.getPrincipal();
					List<String> authorities = principal.getAuthorities().stream().map(item -> item.getAuthority())
							.toList();
					log.info("authorities:{}", authorities);
					context.getClaims().claims(claims -> claims.put("role", new ArrayList<String>(authorities)));
				}
			}
		};
	}

	@Bean
	JWKSource<SecurityContext> jwkSource() {
		RSAKey rsaKey = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey)
				.keyID(UUID.randomUUID().toString()).build();
		JWKSet jwkSet = new JWKSet(rsaKey);
		return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
	}

	@Bean
	JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
		return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
	}

//	@Bean
//	public AuthorizationServerSettings authorizationServerSettings() {
//		return AuthorizationServerSettings.builder().build();
//	}

//	@Bean
//	EmbeddedDatabase embeddedDatabase() {
//		return new EmbeddedDatabaseBuilder().generateUniqueName(true).setType(EmbeddedDatabaseType.H2)
//				.setScriptEncoding("UTF-8")
//				.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
//				.addScript(
//						"org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
//				.addScript(
//						"org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
//				.build();
//	}

}